package redis

import (
	"context"
	"gin-lite/configs"
	"gin-lite/internal/pkg/core"
	"gin-lite/internal/pkg/errors"
	"gin-lite/internal/pkg/timeutil"
	"gin-lite/internal/pkg/trace"
	redisV9 "github.com/redis/go-redis/v9"
	"net"
	"runtime"
	"strconv"
	"sync"
	"time"
)

var (
	client *redisV9.Client
	once   sync.Once
)

type loggingHook struct {
	ts time.Time
}

func (l *loggingHook) DialHook(next redisV9.DialHook) redisV9.DialHook {
	return func(ctx context.Context, network, addr string) (net.Conn, error) {
		return next(ctx, network, addr)
	}
}

func (l *loggingHook) ProcessHook(next redisV9.ProcessHook) redisV9.ProcessHook {
	return func(ctx context.Context, cmd redisV9.Cmder) error {
		stdContext, ok := ctx.(core.StdContext)
		if !ok {
			return errors.New("redis ProcressHook ctx illegal")
		}

		l.ts = time.Now()
		line := fileWithLineNum()
		cmdString := cmd.String()

		err := next(ctx, cmd)
		if err != nil {
			return errors.New("exec redis command err: " + line + " [cmd: " + cmdString + "]")
		}

		if stdContext.Trace != nil {
			redisTrace := new(trace.Redis)
			redisTrace.Time = timeutil.CSTLayoutString()
			redisTrace.Stack = line
			redisTrace.Cmd = cmdString
			redisTrace.CostSeconds = time.Since(l.ts).Seconds()
			stdContext.Trace.AppendRedis(redisTrace)
		}

		return nil
	}
}

func (l *loggingHook) ProcessPipelineHook(next redisV9.ProcessPipelineHook) redisV9.ProcessPipelineHook {
	return func(ctx context.Context, cmds []redisV9.Cmder) error {
		return next(ctx, cmds)
	}
}

func New(conf *configs.Config) *redisV9.Client {
	once.Do(func() {
		redisConf := configs.Get().Redis
		client = redisV9.NewClient(&redisV9.Options{
			Addr:           redisConf.Addr + ":" + redisConf.Port,
			Username:       redisConf.Username,
			Password:       redisConf.Password,
			DB:             redisConf.Db,
			PoolSize:       redisConf.PoolSize,
			MaxActiveConns: redisConf.MaxActiveConns,
		})

		if err := client.Ping(context.Background()).Err(); err != nil {
			panic(err)
		}

		client.AddHook(&loggingHook{})
	})

	return client
}

func Close() error {
	err := client.Close()
	if err != nil {
		return errors.New("关闭数据库redis失败: " + err.Error())
	}

	return nil
}

func fileWithLineNum() string {
	_, file, line, ok := runtime.Caller(5)
	if ok {
		return file + ":" + strconv.FormatInt(int64(line), 10)
	}

	return ""
}
